Skip to content

feat: add vouch and report API endpoints with NIP-98 auth#7

Open
CodyTseng wants to merge 10 commits into
masterfrom
feat/vouch-report-api
Open

feat: add vouch and report API endpoints with NIP-98 auth#7
CodyTseng wants to merge 10 commits into
masterfrom
feat/vouch-report-api

Conversation

@CodyTseng

Copy link
Copy Markdown
Owner

Summary

  • New POST /vouch and POST /report endpoints (NIP-98 authenticated, gated by vouch.enabled) let users contribute to the reputation graph without actually following each other — solving the "I know this new account isn't spam but I don't want to follow them" cold-start problem.
  • Mutual-exclusion toggle: posting one side atomically deletes any existing row from the other side for the same (source, target) pair. There are no DELETE endpoints — reversing a stance requires posting the opposite stance.
  • Graph integration: vouches become equal-weight follow edges, deduped against real follows from the same source (a user who follows and vouches for the same target contributes one edge, not two). Reports apply a trust-weighted penalty to the target's final score: final = raw * (1 - R / (R + F)) where R is the summed trust of trusted reporters and F is the summed trust of the target's incoming edges.
  • Silent admission: submissions from pubkeys with no last-round TrustRank and not in seed_pubkeys return 200 but are not persisted (prevents Sybil inflation while keeping the client unable to probe the admission oracle).
  • API now opens the SQLite DB in ModeReadWrite; WAL mode + the existing writeMu already serialise writes safely across the crawler and API processes. Migration v4 adds the vouches and reports tables.

Test plan

  • go test ./... — 26 new tests pass (11 repository, 11 NIP-98 middleware, 4 ranking)
  • go vet ./... clean
  • go build ./cmd/api and go build ./cmd/crawler produce binaries
  • End-to-end with a real NIP-98 client (curl + signed event) against a dev instance with vouch.enabled: true
  • Verify docker-compose build still succeeds
  • Confirm migration v4 runs cleanly against a pre-existing DB from the previous schema

🤖 Generated with Claude Code

CodyTseng and others added 10 commits April 23, 2026 16:32
Two new POST endpoints (gated by vouch.enabled config) let users submit
signed claims directly instead of through a Nostr event. Vouches act as
equal-weight follow edges in the ranking graph, deduped against any
existing follow from the same source. Reports apply a trust-weighted
penalty to the target's final score: final = raw * (1 - R/(R+F)).

POST /vouch and POST /report are mutually exclusive per (source, target):
posting one atomically removes the opposite side, so there is no DELETE
endpoint — toggling sides is the only way to retract. Submissions from
pubkeys with no TrustRank (and not in seed_pubkeys) return 200 but are
silently dropped to prevent spam-account inflation.

The API now opens the DB in read-write mode; WAL + writeMu already
coordinate it with the crawler. Migration v4 adds vouches and reports
tables. 26 new tests cover NIP-98 middleware, repository mutex/toggle
behaviour, and ranking integration (vouch promotes unfollowed users,
reports decay scores, untrusted reporters are ignored, follow+vouch
edges dedupe).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both API and crawler now open the database read-write, so the
ModeReadOnly branch was dead code. Collapse to a single New(path)
entry point. The API previously inherited the write-mode pool size
of 1, which would have serialized its reads; restore a 10-connection
pool for all callers. Writes are still serialized by writeMu and
SQLite's own locks, so extra connections do not cause contention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A vouch is a weaker signal than an actual follow: the user is asserting
non-spam without committing to see the target's posts. Carry a per-edge
weight through the graph so vouch-only edges contribute proportionally
less flow than a full follow.

Config: vouch.weight (default 0.5, range (0, 1]).

PageRank and TrustRank inner loops now divide by outWeight (sum of
outgoing edge weights) instead of outDegree (count). Each in-edge
carries its own weight; a source's score flows to each target in
proportion to weight / outWeight. Follow and vouch edges share the
same adjacency list — only their weights differ. outDegree is still
tracked as an int so the pubkeys table's Following column remains a
count. New test verifies that lowering the weight produces a lower
score for a vouch-only recipient.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Having both fields required the user to keep them consistent: enabling
with weight=0 (or disabling with weight=0.5) both produced inconsistent
states. Use weight alone — 0 means off (endpoints return 404 and the
ranking calculator skips streaming vouches entirely), > 0 means on and
is the edge weight. Default 0 preserves prior off-by-default behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vouches and reports are now plain signed Nostr events rather than a
NIP-98-wrapped private API, so the data is publishable to any relay and
shared with the wider network. A new internal/ingest package backs both
ingestion paths:

- Crawler (pull): alongside kind:3/0, fetches each author's kind:1984
  reports and kind:10040 vouch sets. Replaceable kinds (3/0/10040) share
  one small-limit query; kind:1984 (append-only) gets its own query
  capped at the newest 50 so it can't crowd out the replaceable events.
- POST /event (push): accepts a single signed event for immediate
  ingestion, keeping the anti-inflation rule (untrusted authors dropped).

Both paths verify signatures before storing. The old /vouch and /report
endpoints and the NIP-98 middleware are retired.

Reports are kind:1984 profile-level (p tag, no e tag) spam/impersonation
events. Vouches are membership in a custom replaceable kind:10040 set and
follow the same lifecycle as follow edges: refreshed via last_seen, never
actively deleted, aged out by the ranking staleness window. Vouch-beats-
report precedence is resolved at ranking time (GetTrustWeightedReports
excludes a reporter who also vouches for the same target).
A source that both vouches for and reports the same target no longer gets
special handling. The vouch adds flow and the report subtracts it at
ranking time, which roughly cancels out on its own — so the extra
NOT EXISTS subquery in GetTrustWeightedReports was redundant complexity.
Vouches now live in a standard NIP-51 follow set (kind:30000) tagged
d=vouch, instead of a custom kind:10040. Other clients can render it as a
people list, and the generic identifier (no project prefix) leaves room
for a shared convention. Other follow sets are ignored.

Because kind:30000 is addressable by d, it can't share the no-d query
with kind:3/0; it gets its own #d-filtered fetch (fetchVouchSetsFromRelay),
and ingest gates it via IsVouchSet.
A REQ carries multiple filters (OR'd), so the kind:3/0 filter and the
kind:30000 vouch-set filter (constrained by #d, scoped to its own filter)
now ride in a single SubManyEose call per relay — one connection, one
round-trip — instead of two separate fetches. kind:1984 reports stay in
their own subscription to keep their per-query limit clean.
- ingest: drop hand-written dTagValue, use go-nostr's Tags.GetD()
- ranking: compute report-penalty F only for reported targets instead of
  scanning the whole graph (drops an O(numNodes) alloc + full edge scan)
- repository/models: remove unused ReportAggregate.NumReporters field and
  its COUNT(*) (only TotalReporterTrust is consumed)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant